package com.zws.cucumber.util.colorfullog;

import org.apache.commons.lang3.SystemUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.Marker;

import java.util.Arrays;
import java.util.Stack;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import static com.zws.cucumber.util.colorfullog.ZwsColorfulLogHelper.*;

/**
 * author: zws
 */
public class ZwsColorfulLogger implements Logger {

    private final Logger origin;

    private final ConcurrentMap<String, String> renderedMap = new ConcurrentHashMap<>();

    private ZwsColorfulLogger(Logger origin) {
        this.origin = origin;
    }

    public static Logger of(Logger origin) {
        return new ZwsColorfulLogger(origin);
    }

    public static Logger of(Class<?> clazz) {
        return new ZwsColorfulLogger(LoggerFactory.getLogger("cucumber." + clazz.getSimpleName()));
    }

    @Override
    public String getName() {
        return origin.getName();
    }

    @Override
    public boolean isTraceEnabled() {
        return origin.isTraceEnabled();
    }

    @Override
    public void trace(String msg) {
        origin.trace(render(msg), msg);
    }

    @Override
    public void trace(String format, Object arg) {
        origin.trace(render(format), arg);
    }

    @Override
    public void trace(String format, Object arg1, Object arg2) {
        origin.trace(render(format), arg1, arg2);
    }

    @Override
    public void trace(String format, Object... arguments) {
        origin.trace(render(format), arguments);
    }

    @Override
    public void trace(String msg, Throwable t) {
        origin.trace(render(msg), t);
    }

    @Override
    public boolean isTraceEnabled(Marker marker) {
        return origin.isTraceEnabled(marker);
    }

    @Override
    public void trace(Marker marker, String msg) {
        origin.trace(marker, render(msg));
    }

    @Override
    public void trace(Marker marker, String format, Object arg) {
        origin.trace(marker, render(format), arg);
    }

    @Override
    public void trace(Marker marker, String format, Object arg1, Object arg2) {
        origin.trace(marker, render(format), arg1, arg2);
    }

    @Override
    public void trace(Marker marker, String format, Object... argArray) {
        origin.trace(marker, render(format), argArray);
    }

    @Override
    public void trace(Marker marker, String msg, Throwable t) {
        origin.trace(marker, render(msg), t);
    }

    @Override
    public boolean isDebugEnabled() {
        return origin.isDebugEnabled();
    }

    @Override
    public void debug(String msg) {
        origin.debug(render(msg));
    }

    @Override
    public void debug(String format, Object arg) {
        origin.debug(render(format), arg);
    }

    @Override
    public void debug(String format, Object arg1, Object arg2) {
        origin.debug(render(format), arg1, arg2);
    }

    @Override
    public void debug(String format, Object... arguments) {
        origin.debug(render(format), arguments);
    }

    @Override
    public void debug(String msg, Throwable t) {
        origin.debug(render(msg), t);
    }

    @Override
    public boolean isDebugEnabled(Marker marker) {
        return origin.isDebugEnabled(marker);
    }

    @Override
    public void debug(Marker marker, String msg) {
        origin.debug(marker, render(msg));
    }

    @Override
    public void debug(Marker marker, String format, Object arg) {
        origin.debug(marker, render(format), arg);
    }

    @Override
    public void debug(Marker marker, String format, Object arg1, Object arg2) {
        origin.debug(marker, render(format), arg1, arg2);
    }

    @Override
    public void debug(Marker marker, String format, Object... arguments) {
        origin.debug(marker, render(format), arguments);
    }

    @Override
    public void debug(Marker marker, String msg, Throwable t) {
        origin.debug(marker, render(msg), t);
    }

    @Override
    public boolean isInfoEnabled() {
        return origin.isInfoEnabled();
    }

    @Override
    public void info(String msg) {
        origin.info(render(msg));
    }

    @Override
    public void info(String format, Object arg) {
        origin.info(render(format), arg);
    }

    @Override
    public void info(String format, Object arg1, Object arg2) {
        origin.info(render(format), arg1, arg2);
    }

    @Override
    public void info(String format, Object... arguments) {
        origin.info(render(format), arguments);
    }

    @Override
    public void info(String msg, Throwable t) {
        origin.info(render(msg), t);
    }

    @Override
    public boolean isInfoEnabled(Marker marker) {
        return origin.isInfoEnabled(marker);
    }

    @Override
    public void info(Marker marker, String msg) {
        origin.info(marker, render(msg));
    }

    @Override
    public void info(Marker marker, String format, Object arg) {
        origin.info(marker, render(format), arg);
    }

    @Override
    public void info(Marker marker, String format, Object arg1, Object arg2) {
        origin.info(marker, render(format), arg1, arg2);
    }

    @Override
    public void info(Marker marker, String format, Object... arguments) {
        origin.info(marker, render(format), arguments);
    }

    @Override
    public void info(Marker marker, String msg, Throwable t) {
        origin.info(marker, render(msg), t);
    }

    @Override
    public boolean isWarnEnabled() {
        return origin.isWarnEnabled();
    }

    @Override
    public void warn(String msg) {
        origin.warn(render(msg));
    }

    @Override
    public void warn(String format, Object arg) {
        origin.warn(render(format), arg);
    }

    @Override
    public void warn(String format, Object... arguments) {
        origin.warn(render(format), arguments);
    }

    @Override
    public void warn(String format, Object arg1, Object arg2) {
        origin.warn(render(format), arg1, arg2);
    }

    @Override
    public void warn(String msg, Throwable t) {
        origin.warn(render(msg), t);
    }

    @Override
    public boolean isWarnEnabled(Marker marker) {
        return origin.isWarnEnabled(marker);
    }

    @Override
    public void warn(Marker marker, String msg) {
        origin.warn(marker, render(msg));
    }

    @Override
    public void warn(Marker marker, String format, Object arg) {
        origin.warn(marker, render(format), arg);
    }

    @Override
    public void warn(Marker marker, String format, Object arg1, Object arg2) {
        origin.warn(marker, render(format), arg1, arg2);
    }

    @Override
    public void warn(Marker marker, String format, Object... arguments) {
        origin.warn(marker, render(format), arguments);
    }

    @Override
    public void warn(Marker marker, String msg, Throwable t) {
        origin.warn(marker, render(msg), t);
    }

    @Override
    public boolean isErrorEnabled() {
        return origin.isErrorEnabled();
    }

    @Override
    public void error(String msg) {
        origin.error(render(msg));
    }

    @Override
    public void error(String format, Object arg) {
        origin.error(render(format), arg);
    }

    @Override
    public void error(String format, Object arg1, Object arg2) {
        origin.error(render(format), arg1, arg2);
    }

    @Override
    public void error(String format, Object... arguments) {
        origin.error(render(format), arguments);
    }

    @Override
    public void error(String msg, Throwable t) {
        origin.error(render(msg), t);
    }

    @Override
    public boolean isErrorEnabled(Marker marker) {
        return origin.isErrorEnabled(marker);
    }

    @Override
    public void error(Marker marker, String msg) {
        origin.error(marker, render(msg));
    }

    @Override
    public void error(Marker marker, String format, Object arg) {
        origin.error(marker, render(format), arg);
    }

    @Override
    public void error(Marker marker, String format, Object arg1, Object arg2) {
        origin.error(marker, render(format), arg1, arg2);
    }

    @Override
    public void error(Marker marker, String format, Object... arguments) {
        origin.error(marker, render(format), arguments);
    }

    @Override
    public void error(Marker marker, String msg, Throwable t) {
        origin.error(marker, render(msg), t);
    }


    private String render(String source) {
        return renderedMap.computeIfAbsent(source, this::parse);
    }

    private String parse(String source) {
        Stack<String> stack = new Stack<>();
        StringBuilder sb = new StringBuilder(source.length() * 2);
        sb.append(CucumberCtTag);
        for (int i = 0; i < source.length(); i++) {
            char c = source.charAt(i);
            if (c == '<') {
                int nextIndex = -1;
                if (lookahead(source, i, "</") >= 0) {
                    nextIndex = popAndRenderIfMatch(stack, sb, source, i,
                            Pair.of("</black>", FG_BLACK),
                            Pair.of("</r>", FG_RED),
                            Pair.of("</g>", FG_GREEN),
                            Pair.of("</y>", FG_YELLOW),
                            Pair.of("</b>", FG_BLUE),
                            Pair.of("</p>", FG_PURPLE),
                            Pair.of("</c>", FG_CYAN),
                            Pair.of("</white>", FG_WHITE),
                            Pair.of("</bold>", BOLD),
                            Pair.of("</faint>", FAINT),
                            Pair.of("</italic>", ITALIC),
                            Pair.of("</underline>", UNDERLINE));
                } else {
                    nextIndex = pushAndRenderIfMatch(stack, sb, source, i,
                            Pair.of("<black>", FG_BLACK),
                            Pair.of("<r>", FG_RED),
                            Pair.of("<g>", FG_GREEN),
                            Pair.of("<y>", FG_YELLOW),
                            Pair.of("<b>", FG_BLUE),
                            Pair.of("<p>", FG_PURPLE),
                            Pair.of("<c>", FG_CYAN),
                            Pair.of("<white>", FG_WHITE),
                            Pair.of("<bold>", BOLD),
                            Pair.of("<faint>", FAINT),
                            Pair.of("<italic>", ITALIC),
                            Pair.of("<underline>", UNDERLINE));
                }
                if (nextIndex >= 0) {
                    i = nextIndex - 1;
                } else {
                    sb.append(c);
                }

            } else {
                sb.append(c);
            }
        }
        if (!stack.isEmpty()) {
            System.out.println(stack);
            throw new IllegalArgumentException("not empty stack");
        }
        return sb.toString();
    }

    private int lookahead(String source, int startIndex, String searchStr) {
        if (startIndex + searchStr.length() > source.length()) {
            return -1;
        }
        for (int i = 0; i < searchStr.length(); i++) {
            if (source.charAt(startIndex + i) != searchStr.charAt(i)) {
                return -1;
            }
        }
        return startIndex + searchStr.length();
    }

    private int pushAndRenderIfMatch(Stack<String> stack, StringBuilder target, String source, int startIndex, Pair<String, String> ... literalAndFlags) {
        for (Pair<String, String> literalAndFlag : literalAndFlags) {
            int nextIndex = -1;
            if ((nextIndex = lookahead(source, startIndex, literalAndFlag.getKey())) >= 0) {
                String flag = literalAndFlag.getValue();
                stack.push(flag);
                if (SystemUtils.IS_OS_WINDOWS) {
                    target.append(ZwsColorfulLogHelper.escape(flag));
                }
                return nextIndex;
            }
        }
        return -1;
    }

    private int popAndRenderIfMatch(Stack<String> stack, StringBuilder target, String source, int startIndex, Pair<String, String> ... literalAndFlags) {
        for (Pair<String, String> literalAndFlag : literalAndFlags) {
            int nextIndex = -1;
            if ((nextIndex = lookahead(source, startIndex, literalAndFlag.getKey())) >= 0) {
                String flag = literalAndFlag.getValue();
                if (stack.peek() != flag) {
                    throw new IllegalArgumentException("unexpected literal: " + literalAndFlag.getKey() + ", flag: " + flag + ", flag in stack: " + stack.peek());
                }
                stack.pop();
                if (SystemUtils.IS_OS_WINDOWS) {
                    if (stack.isEmpty()) {
                        target.append(ZwsColorfulLogHelper.ANSI_RESET);
                    } else {
                        target.append(renderFromOrigin(stack));
                    }
                }
                return nextIndex;
            }
        }
        return -1;
    }

    private String renderFromOrigin(Stack<String> stack) {
        final int c = 0, b = 1, i = 2, u = 3;
        String[] flags = new String[4];
        Arrays.fill(flags, null);
        for (String flag : stack) {
            int idx = switch (flag) {
                case FG_BLACK, FG_RED, FG_GREEN, FG_YELLOW, FG_BLUE, FG_PURPLE, FG_CYAN, FG_WHITE -> c;
                case BOLD, FAINT -> b;
                case ITALIC -> i;
                case UNDERLINE -> u;
                default -> throw new IllegalArgumentException("unknown flag: " + flag);
            };
            flags[idx] = flag;
        }
        return ANSI_RESET + ZwsColorfulLogHelper.escape(flags);
    }

    public static void main(String[] args) {
        ZwsColorfulLogger logger = new ZwsColorfulLogger(null);
        System.out.println(logger.parse("<b>this is <y>yellow</y> and that is <g>green</g></b>. They are <r><underline><bold><italic>c<b>o</b><faint><g>l</g><c>o</c></faint><y>r</y><white>f</white><p>u</p>l</italic></bold></underline></r>."));
    }
}
